﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;

namespace EntModApp.EntServices.Business
{
  [Serializable]
  public abstract class BusinessObjectCollectionBase<T> : 
    Collection<T>,
    IBusinessObjectCollection<T>
    where T : class, IBusinessObject
  {
    private const string CountName = "Count";
    private const string IndexerName = "Item[]";

    private SimpleMonitor monitor;
    private SimpleMonitor eventsMonitor;

    protected BusinessObjectCollectionBase()
    {
      monitor = new SimpleMonitor(this);
      eventsMonitor = new SimpleMonitor(this);
    }

    protected override void ClearItems()
    {
      CheckReentrancy();
      base.ClearItems();
      OnPropertyChanged(CountName);
      OnPropertyChanged(IndexerName);
      OnCollectionReset();
    }
    
    protected override void InsertItem(int index, T item)
    {
      CheckReentrancy();
      base.InsertItem(index, item);
      OnPropertyChanged(CountName);
      OnPropertyChanged(IndexerName);
      OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index);
    }

    public void Move(int oldIndex, int newIndex)
    {
      this.MoveItem(oldIndex, newIndex);
    }

    protected virtual void MoveItem(int oldIndex, int newIndex)
    {
      CheckReentrancy();
      T item = base[oldIndex];
      base.RemoveItem(oldIndex);
      base.InsertItem(newIndex, item);
      OnPropertyChanged(IndexerName);
      OnCollectionChanged(NotifyCollectionChangedAction.Move, item, newIndex, oldIndex);
    }

    protected override void RemoveItem(int index)
    {
      CheckReentrancy();
      T item = base[index];
      base.RemoveItem(index);
      OnPropertyChanged(CountName);
      OnPropertyChanged(IndexerName);
      OnCollectionChanged(NotifyCollectionChangedAction.Remove, item, index);
    }

    protected override void SetItem(int index, T item)
    {
      CheckReentrancy();
      T t = base[index];
      base.SetItem(index, item);
      OnPropertyChanged(IndexerName);
      OnCollectionChanged(NotifyCollectionChangedAction.Replace, t, item, index);
    }

    public void CopyFrom(IEnumerable<T> e, bool append = false)
    {
      eventsMonitor.Enter();
      using (eventsMonitor)
      {
        if (!append) 
          Clear();
        foreach (var item in e)
          Add(item);
      }
    }

    public abstract bool IsReadonly { get; }

    public abstract bool IsDirty { get; }

    public abstract int TotalItems { get; }

    [field: NonSerialized]
    public event NotifyCollectionChangedEventHandler CollectionChanged;
    protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e)
    {
      if (eventsMonitor.IsBusy)
        eventsMonitor.ModifyCollection();
      else
        OnCollectionChangedInternal(e);
    }
    private void OnCollectionChangedInternal(NotifyCollectionChangedEventArgs e)
    {
      var handler = this.CollectionChanged;
      if (handler != null)
        using (BlockReentrancy())
          handler(this, e);
    }
    private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index)
    {
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index));
    }
    private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index, int oldIndex)
    {
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index, oldIndex));
    }
    private void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index)
    {
      this.OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index));
    }
    protected void OnCollectionReset()
    {
      OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }
    private void OnCollectionResetInternal()
    {
      OnCollectionChangedInternal(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
    }

    [field: NonSerialized]
    public event PropertyChangedEventHandler PropertyChanged;
    protected virtual void OnPropertyChanged(PropertyChangedEventArgs e)
    {
      if (eventsMonitor.IsBusy)
        eventsMonitor.ModifyCollection();
      else
        OnPropertyChangedInternal(e);
    }
    protected void OnPropertyChangedInternal(string propertyName)
    {
      OnPropertyChanged(new PropertyChangedEventArgs(propertyName));
    }
    private void OnPropertyChangedInternal(PropertyChangedEventArgs e)
    {
      var handler = this.PropertyChanged;
      if (handler != null)
        handler(this, e);
    }
    private void OnPropertyChanged(string propertyName)
    {
      OnPropertyChangedInternal(new PropertyChangedEventArgs(propertyName));
    }

    protected IDisposable BlockReentrancy()
    {
      monitor.Enter();
      return monitor;
    }

    protected void CheckReentrancy()
    {
      if (!monitor.IsBusy || this.CollectionChanged == null || this.CollectionChanged.GetInvocationList().Length <= 1)
        return;
      throw new InvalidOperationException("Collection reentrancy not allowed");
    }

    class SimpleMonitor : IDisposable
    {
      BusinessObjectCollectionBase<T> owner;

      private int busyCount;
      private bool modifiedCollection;

      public SimpleMonitor(BusinessObjectCollectionBase<T> owner)
      {
        this.owner = owner;
        busyCount = 0;
      }

      public bool IsBusy { get { return busyCount > 0; } }

      public void ModifyCollection()
      {
        modifiedCollection = true;
      }

      public void Enter()
      {
        var monitor = this;
        if (monitor.busyCount == 0)
          modifiedCollection = false;
        monitor.busyCount++;
      }

      public void Dispose()
      {
        var monitor = this;
        monitor.busyCount--;
        if (monitor.busyCount == 0)
        {
          if (modifiedCollection)
          {
            owner.OnPropertyChangedInternal(CountName);
            owner.OnPropertyChangedInternal(IndexerName);
            owner.OnCollectionResetInternal();
          }
          modifiedCollection = false;
        }
      }
    }
  }
}
